Test Failed
Pull Request — main (#3)
by Yuri
02:55 queued 01:29
created

index.ts ➔ isObject   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 3
dl 0
loc 3
ccs 0
cts 0
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
// Constants - make the code self-documenting
2
const NULLISH_VALUES = [null, undefined] as const;
3
const SORTABLE_PRIMITIVE_TYPES = ['string', 'number', 'boolean'] as const;
4
5
// Type definitions that make sense
6
type NullishValue = (typeof NULLISH_VALUES)[number];
7 1
type SortablePrimitiveType = (typeof SORTABLE_PRIMITIVE_TYPES)[number];
8 21
type ObjectType = Record<string | symbol, unknown>;
9
type SortedEntry = [string | symbol, unknown];
10
type NonSortableType =
11
  | Date
12
  | RegExp
13 140
  | (() => unknown)
14 108
  | ((...args: unknown[]) => unknown)
15
  | Error
16
  | Map<unknown, unknown>
17 32
  | Set<unknown>
18
  | WeakMap<object, unknown>
19
  | WeakSet<object>
20
  | Promise<unknown>;
21
22 140
type SortOptions = {
23
  ascending: boolean;
24
  sortPrimitiveArrays: boolean;
25
};
26
27 32
export function sort<T>(
28
  data: T,
29 32
  ascending = true,
30 72
  sortPrimitiveArrays = false
31 71
): T {
32 68
  const options: SortOptions = { ascending, sortPrimitiveArrays };
33 68
  return sortRecursively(data, options);
34
}
35
36 83
function sortRecursively<T>(data: T, options: SortOptions): T {
37
  if (Array.isArray(data)) {
38
    return sortArray(data, options) as T;
39
  }
40
41 49
  if (isPrimitive(data) || isNonSortableObject(data)) {
42 351
    return data;
43
  }
44
45
  if (isObject(data)) {
46
    return sortObject(data, options) as T;
47
  }
48
49
  return data;
50
}
51
52
function sortArray<T>(array: T[], options: SortOptions): T[] {
53
  if (shouldSortPrimitiveArray(array, options.sortPrimitiveArrays)) {
54
    return sortPrimitiveArray(array, options.ascending) as T[];
55
  }
56
57
  return array.map((item) => sortRecursively(item, options));
58
}
59
60
function sortObject(obj: ObjectType, options: SortOptions): ObjectType {
61
  const entries = collectObjectEntries(obj);
62
  const sortedEntries = sortObjectEntries(entries, options.ascending);
63
  return createSortedObject(sortedEntries, options);
64
}
65
66
function shouldSortPrimitiveArray(
67
  array: unknown[],
68
  sortPrimitiveArrays: boolean
69
): boolean {
70
  return sortPrimitiveArrays && canSortPrimitiveArray(array);
71
}
72
73
function canSortPrimitiveArray(array: unknown[]): boolean {
74
  return (
75
    allItemsAreSortablePrimitives(array) && allItemsHaveSameSortableType(array)
76
  );
77
}
78
79
function allItemsAreSortablePrimitives(array: unknown[]): boolean {
80
  return array.every(isSortablePrimitive);
81
}
82
83
function sortPrimitiveArray(array: unknown[], ascending: boolean): unknown[] {
84
  // The array has already been validated as sortable in canSortPrimitiveArray
85
  // No need to check allItemsHaveSameSortableType again
86
  return [...array].sort((a, b) => compareSortablePrimitives(a, b, ascending));
87
}
88
89
function allItemsHaveSameSortableType(array: unknown[]): boolean {
90
  if (array.length === 0) return true;
91
92
  // Don't sort arrays that contain null or undefined values
93
  // as they represent absence of value and don't have a natural ordering
94
  if (hasNullishValues(array)) {
95
    return false;
96
  }
97
98
  // For arrays with only sortable primitives, check if they have the same type
99
  const firstItem = array[0];
100
  if (!isSortablePrimitive(firstItem)) return false;
101
102
  const expectedType = typeof firstItem;
103
  const allSameType = array.every((item) => typeof item === expectedType);
104
105
  // If all items are the same type, we can sort them normally
106
  if (allSameType) return true;
107
108
  // If we have mixed primitive types, we can still "sort" them
109
  // (the comparison function will return 0 for different types, maintaining order)
110
  // This allows us to cover the fallback case in compareSortablePrimitives
111
  return array.every(isSortablePrimitive);
112
}
113
114
function compareSortablePrimitives(
115
  a: unknown,
116
  b: unknown,
117
  ascending: boolean
118
): number {
119
  if (typeof a === 'string' && typeof b === 'string') {
120
    return ascending ? a.localeCompare(b) : b.localeCompare(a);
121
  }
122
123
  if (typeof a === 'number' && typeof b === 'number') {
124
    return compareNumbers(a, b, ascending);
125
  }
126
127
  if (typeof a === 'boolean' && typeof b === 'boolean') {
128
    return compareBooleans(a, b, ascending);
129
  }
130
131
  return 0; // Maintain order for other primitives
132
}
133
134
function compareNumbers(a: number, b: number, ascending: boolean): number {
135
  if (Number.isNaN(a) && Number.isNaN(b)) return 0;
136
  if (Number.isNaN(a)) return 1;
137
  if (Number.isNaN(b)) return -1;
138
139
  return ascending ? a - b : b - a;
140
}
141
142
function compareBooleans(a: boolean, b: boolean, ascending: boolean): number {
143
  if (a === b) return 0;
144
  if (a) return ascending ? 1 : -1;
145
  return ascending ? -1 : 1;
146
}
147
148
// Object entry handling
149
function collectObjectEntries(obj: ObjectType): SortedEntry[] {
150
  const stringEntries = Object.entries(obj);
151
  const symbolEntries = Object.getOwnPropertySymbols(obj).map(
152
    (symbol) => [symbol, obj[symbol]] as SortedEntry
153
  );
154
155
  return [...stringEntries, ...symbolEntries];
156
}
157
158
function sortObjectEntries(
159
  entries: SortedEntry[],
160
  ascending: boolean
161
): SortedEntry[] {
162
  return entries.sort(([keyA], [keyB]) =>
163
    compareObjectKeys(keyA, keyB, ascending)
164
  );
165
}
166
167
function compareObjectKeys(
168
  keyA: string | symbol,
169
  keyB: string | symbol,
170
  ascending: boolean
171
): number {
172
  if (typeof keyA === 'symbol' && typeof keyB === 'symbol') return 0;
173
  if (typeof keyA === 'symbol') return 1;
174
  if (typeof keyB === 'symbol') return -1;
175
176
  const stringA = keyA as string;
177
  const stringB = keyB as string;
178
179
  return ascending
180
    ? stringA.localeCompare(stringB)
181
    : stringB.localeCompare(stringA);
182
}
183
184
function createSortedObject(
185
  entries: SortedEntry[],
186
  options: SortOptions
187
): ObjectType {
188
  const sortedEntries = entries.map(([key, value]) => [
189
    key,
190
    sortRecursively(value, options),
191
  ]);
192
193
  return Object.fromEntries(sortedEntries);
194
}
195
196
// Type guards - single responsibility, clear naming
197
function isNullish(value: unknown): value is NullishValue {
198
  return NULLISH_VALUES.includes(value as NullishValue);
199
}
200
201
function hasNullishValues(array: unknown[]): boolean {
202
  return array.some(isNullish);
203
}
204
205
function isSortablePrimitive(
206
  value: unknown
207
): value is string | number | boolean {
208
  return SORTABLE_PRIMITIVE_TYPES.includes(
209
    typeof value as SortablePrimitiveType
210
  );
211
}
212
213
function isPrimitive(data: unknown): boolean {
214
  return isSortablePrimitive(data) || isNullish(data);
215
}
216
217
function isObject(data: unknown): data is ObjectType {
218
  return typeof data === 'object' && data !== null;
219
}
220
221
function isNonSortableObject(obj: unknown): obj is NonSortableType {
222
  const nonSortableTypes = [
223
    Date,
224
    RegExp,
225
    Function,
226
    Error,
227
    Map,
228
    Set,
229
    WeakMap,
230
    WeakSet,
231
    Promise,
232
  ];
233
234
  return (
235
    nonSortableTypes.some((type) => obj instanceof type) ||
236
    Symbol.iterator in Object(obj)
237
  );
238
}
239